Skip to content

this 绑定与隐式丢失

作者:青见春山
发表于:2026-07-29
字数统计:4500 字
预计阅读16分钟

涵盖 this 四种绑定规则、隐式丢失的本质、bind/箭头函数/保存 self 三种解决方案。

一、this 绑定四种规则

JavaScript
var name = '全局';
var obj = {
    name: '对象',
    sayName: function() {
        console.log(this.name)
    }
};

// 隐式绑定(对象方法调用)
obj.sayName();  // '对象'

// 隐式丢失(独立调用)
var bar = obj.sayName
bar();  // '全局'

this 绑定规则

调用方式this
obj.foo()(对象前缀)obj
bar()(无前缀)全局(非严格模式)/ undefined(严格模式)
new Foo()新创建的实例
foo.call(obj) / apply / bindobj(显式指定)

二、隐式丢失的本质

JavaScript
var a = 1
function foo() {
    var a = 3
    console.log(this.a)
}
var obj = {
    a: 2,
    foo: foo
}

// 关键步骤分析
var bar = obj.foo  // bar 只是 foo 函数的另一个引用
bar()              // 独立调用,this 指向全局

bar 不是一个对象,它只是一个变量,存储的是函数的引用。调用 bar() 时,实际上是独立函数调用,而不是作为对象的方法调用。

等价于

JavaScript
// 1. obj.foo 获取到 foo 函数的引用
// 2. 把这个引用赋值给 bar
// 3. bar() 相当于直接调用 foo()
// 4. 独立函数调用,this 指向全局

内存中的关系

JavaScript
function foo() { ... }  // 函数对象存储在内存中

// 多个引用指向同一个函数
obj.foo = foo      // obj 的 foo 属性指向函数
var bar = foo      // bar 变量也指向同一个函数

// 调用方式决定 this
obj.foo()  // 隐式绑定,this = obj
bar()      // 独立调用,this = 全局
foo()      // 独立调用,this = 全局

三、详细的执行过程

JavaScript
var a = 1
function foo() {
    var a = 3
    console.log(this.a)
}
var obj = {
    a: 2,
    foo: foo  // obj.foo 引用 foo 函数
}

// 步骤1:获取 obj.foo 的值(这是一个函数引用)
var bar = obj.foo  // bar 现在持有这个函数引用

// 步骤2:调用 bar()
bar()  // 相当于直接调用函数,没有任何对象前缀
        // 这是独立函数调用,this 指向全局对象
        // 所以 this.a = window.a = 1

四、为什么 bar 不是新对象?

JavaScript
var bar = obj.foo
console.log(typeof bar)  // "function",不是 "object"

// 关键:调用时没有对象前缀
bar();  // 没有使用 . 或 [] 调用,所以是独立调用

五、各种调用方式对比

JavaScript
var a = 1
var obj = {
    a: 2,
    foo: function() {
        console.log(this.a)
    }
};

// 情况1:隐式绑定(对象方法调用)
obj.foo()  // 2 - 有对象前缀,this = obj

// 情况2:独立调用(函数引用)
var bar = obj.foo
bar()      // 1 - 没有对象前缀,this = 全局

// 情况3:独立调用(直接调用)
var func = obj.foo
func()     // 1

// 情况4:立即调用(仍然是独立调用)
(obj.foo)()                    // 2 - 括号不影响,仍然是 obj.foo()
(obj.foo = obj.foo)()          // 1 - 赋值表达式返回函数本身,独立调用

六、更深入的例子

JavaScript
var name = '全局'
var obj = {
    name: '对象',
    sayName: function() {
        console.log(this.name)
    }
}

// 示例1:作为参数传递
function run(fn) {
    fn()  // 独立调用,this = 全局
}
run(obj.sayName)  // 输出: 全局

// 示例2:作为返回值
function getFn() {
    return obj.sayName
}
var fn = getFn()
fn()  // 输出: 全局

// 示例3:数组中的函数
var arr = [obj.sayName]
arr[0]()  // 输出: 全局 - 通过数组索引调用也是独立调用

// 示例4:setTimeout 中的函数
setTimeout(obj.sayName, 100)  // 输出: 全局 - 回调是独立调用

七、参数传递的本质

JavaScript
// 在 JavaScript 中,参数传递是值传递(对函数来说传递的是引用值)

var obj = { value: 1 }

function test(param) {
    // param 接收的是 obj 引用的副本
    // 但函数引用也是值
    param()
}

// 等价于:
var temp = obj.foo  // 获取函数引用
test(temp)          // 传递引用副本

// 在 test 内部
function test(param) {
    // param 指向同一个函数
    // 调用时没有对象上下文
    param()
}

八、避免隐式丢失的三种方法

方法1:使用 bind 绑定

JavaScript
var obj = {
    a: 2,
    foo: function() {
        console.log(this.a)
    }
}

var bar = obj.foo.bind(obj)
bar()  // 2

方法2:使用箭头函数

JavaScript
var obj2 = {
    a: 2,
    foo: function() {
        setTimeout(() => {
            console.log(this.a)  // 箭头函数捕获外层 this
        }, 100)
    }
}
obj2.foo()  // 2

方法3:保存 this 引用

JavaScript
var obj3 = {
    a: 2,
    foo: function() {
        var self = this
        setTimeout(function() {
            console.log(self.a)  // 使用保存的引用
        }, 100)
    }
}
obj3.foo()  // 2

九、判断 this 的规则

  • 有对象前缀(obj.foo())→ this = 该对象
  • 无对象前缀(bar())→ this = 全局对象(非严格模式)或 undefined(严格模式)

这就是为什么 bar() 输出全局的 1,而不是对象的 2。

十一、变量提升 vs 函数声明提升

核心规则

  1. 函数声明优先于变量声明提升:函数声明会被提升到最顶端,变量声明次之。
  2. var 只提升声明,不提升赋值:提升后初始值为 undefined
  3. 同名函数声明,后面的会覆盖前面的

经典案例分析

JavaScript
function foo() {
    console.log('foo1')
}
foo() // foo2

var foo = function() {
    console.log('foo2')
}
foo() // foo2

function foo() {
    console.log('foo3')
}
foo() // foo3(最终 foo 函数体)

预编译阶段

  1. 函数声明 function foo() 提升(两个都会被提升,后面的覆盖前面的),最终 foo 函数体为 console.log('foo3')
  2. var foo 提升(但函数声明优先),但因为 foo 已经被赋值为函数,重复声明会被忽略。
  3. var foo = function() { console.log('foo2') } 这行赋值在执行阶段才发生。

执行阶段

  • 第 1 行:foo() 输出 'foo2'(覆盖前最终函数体)。
  • 第 7 行:执行 var foo = function() {...},把 foo 重新赋值为函数表达式。
  • 第 12 行:foo() 输出 'foo3'(?? 因为函数声明 function foo() 已经被上面的赋值覆盖了)

这道题的争议点:实际中 V8 引擎对函数提升和赋值提升的处理有优化行为,不同环境可能略有差异。核心考点是:函数声明 + 同名 var 会被覆盖

关键纠错点

  • 不要把 var foo = function() {}function foo() {} 等同——前者是"声明+赋值",后者只是"声明"。
  • 多个同名 function 声明,提升时是后面的覆盖前面的。
  • var 声明的变量如果和函数同名,函数优先。

十二、作用域 + this + 运算符优先级 + new 绑定规则

案例

JavaScript
var name = 'window'
function Foo(name) {
    this.name = name
}
Foo.prototype.getName = function() {
    return this.name
}
function getName() {
    console.log(this.name)
}

getName()                    // ?
Foo.prototype.getName()      // ?
var getName = Foo.prototype.getName
getName()                    // ?
new Foo('foo').getName()     // ?

答案

  1. getName()'window'(全局调用,this 是 window,name 是全局 var name
  2. Foo.prototype.getName()undefined(this 是原型对象,原型没有 name
  3. var getName = Foo.prototype.getName; getName()'window'(赋值后再调用,this 是 window)
  4. new Foo('foo').getName()'foo'(new 出来的实例 name = 'foo',this 指向实例)

核心考点

  • 变量污染var name = 'window' 声明在全局,全局 getName 调用时会读取它。
  • this 指向:决定 this.name 读哪个 name
  • 运算符优先级new Foo('foo').getName()new Foo('foo') 先执行,然后 .getName() 访问实例方法。
  • 原型链查找Foo.prototype.getName 实际上是通过原型链给实例添加方法。

一句话总结

this 指向取决于调用方式(对象方法 / 全局 / call / apply / bind / new),而变量提升和函数声明提升决定函数 / 变量在哪个作用域里能被访问到

十三、关联文档